﻿namespace Contoso.ActiveDirectory {

    #region Usings
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.DirectoryServices.ActiveDirectory;
    using System.Linq;
    using System.Security.Principal;
    using System.Text;
    using System.Threading.Tasks;
    #endregion

    /// <summary>
    /// Frequently used domain properties
    /// </summary>
    public class DomainConfiguration {

        #region Members
        public string DistinguishedName { get; private set; }

        public string DNSName { get; private set; }

        /// <summary>
        /// Fully-qualified dns name of the domain controllers
        /// </summary>
        public List<string> DomainDCs {
            get {
                if ((domainDCs == null) || (domainDCs.Count == 0)) {
                    lock (SyncRoot) {
                        if ((domainDCs == null) || (domainDCs.Count == 0)) {
                            domainDCs = new List<string>();

                            lock (ConnectionManager.AvailableDCs) {
                                if ((ConnectionManager.AvailableDCs.Count > 0) && (ConnectionManager.AvailableDCs.Any(x => string.Equals(x.Key, this.NetbiosName, StringComparison.OrdinalIgnoreCase)))) {
                                    domainDCs = ConnectionManager.AvailableDCs[this.NetbiosName]
                                        .Select(x => x.DNSName)
                                        .ToList();
                                }
                            }
                        }
                    }
                }

                return domainDCs;
            }
        }
        private List<string> domainDCs;

        public Guid DomainGuid { get; private set; }

        public string ForestName { get; private set; }

        public bool? IsCurrentComputerDomain {
            get {
                if (!isCurrentComputerDomain.HasValue) {
                    try {
                        Domain domain = Domain.GetComputerDomain();
                        isCurrentComputerDomain = string.Equals(domain.Name, this.DNSName, StringComparison.OrdinalIgnoreCase);
                    }
                    catch (Exception e) {
                        Debug.WriteLine(string.Format("{0}  {1} {2}", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                    }
                }

                return isCurrentComputerDomain;
            }
        }
        private bool? isCurrentComputerDomain;

        public bool? IsCurrentUserDomain {
            get {
                if (!isCurrentUserDomain.HasValue) {
                    try {
                        Domain domain = Domain.GetCurrentDomain();
                        isCurrentUserDomain = string.Equals(domain.Name, this.DNSName, StringComparison.OrdinalIgnoreCase);
                    }
                    catch (Exception e) {
                        Debug.WriteLine(string.Format("{0}  {1} {2}", DateTime.Now.TimeOfDay, CustomExtensions.CurrentMethodName(), e.VerboseExceptionString()));
                    }
                }

                return isCurrentUserDomain;
            }
        }
        private bool? isCurrentUserDomain;

        public bool IsRootDomain { get; private set; }

        public string NetbiosName { get; private set; }

        /// <summary>
        /// Fully-qualified dns name of the preferred domain controller
        /// </summary>
        public string PreferredDC {
            get {
                preferredDC = this.DomainDCs
                    .Where(x => ConnectionManager.PreferredDCGC.Any(
                        y => x.StartsWith(y, StringComparison.OrdinalIgnoreCase)))
                    .FirstOrDefault();

                if (string.IsNullOrWhiteSpace(preferredDC)) {
                    preferredDC = this.DomainDCs
                        .Where(x => this.PDCEmulatorOwner.ToUpper().Contains(x.Substring(0, x.IndexOf(".")).ToUpper()))
                        .FirstOrDefault();
                }
                if (string.IsNullOrWhiteSpace(preferredDC)) {
                    preferredDC = this.DomainDCs
                        .FirstOrDefault();
                }

                return preferredDC;
            }
        }
        private string preferredDC;

        public SecurityIdentifier SID { get; private set; }

        private readonly object SyncRoot = new object();

        #region FSMO Role Properties
        /* FSMO Role information
        /// <summary>
        /// Operations master roles
        /// http://technet.microsoft.com/en-us/library/cc773108%28v=ws.10%29.aspx
        /// FSMO Role Attributes are recorded in the fSMORoleOwner attribute at the following locations:
        /// 
        /// Forest-Wide:
        /// 
        /// Schema Master FSMO:
        /// LDAP://CN=Schema,CN=Configuration,DC=Microsoft,DC=Com
        /// 
        /// Domain Naming Master FSMO:
        /// LDAP://CN=Partitions,CN=Configuration,DC=Microsoft,DC=Com 
        /// 
        /// Per-Domain:
        /// 
        /// Primary Domain Controller (PDC) FSMO:
        /// LDAP://DC=MICROSOFT,DC=COM
        /// 
        /// RID Master FSMO:
        /// LDAP://CN=Rid Manager$,CN=System,DC=MICROSOFT,DC=COM
        /// 
        /// Infrastructure Master FSMO:
        /// LDAP://CN=Infrastructure,DC=Microsoft,DC=Com
        /// </summary>
        */

        public string DomainNamingMasterOwner {
            get {
                if (IsRootDomain) {
                    var filter = string.Format("(objectCategory={0})", DomainNamingMaster.ObjectCategoryShortName);
                    var domainNamingMasterList = ADDataAccess.GetADObjectProperties<DomainNamingMaster>(
                        ADDataAccess.DomainNamingMasterDN, null, filter, PartitionScope.LocalPartition);

                    if ((domainNamingMasterList != null) && (domainNamingMasterList.Count == 1)) {
                        domainNamingMasterFSMO = domainNamingMasterList.Single().FSMORoleOwner;
                    }
                }

                return domainNamingMasterFSMO;
            }
        }
        private string domainNamingMasterFSMO;

        public string SchemaMasterOwner {
            get {
                if (IsRootDomain) {
                    var filter = string.Format("(objectCategory={0})", SchemaMaster.ObjectCategoryShortName);
                    var schemaMasterList = ADDataAccess.GetADObjectProperties<SchemaMaster>(
                        ADDataAccess.SchemaMasterDN, null, filter, PartitionScope.LocalPartition);

                    if ((schemaMasterList != null) && (schemaMasterList.Count == 1)) {
                        schemaMasterOwner = schemaMasterList.Single().FSMORoleOwner;
                    }
                }

                return schemaMasterOwner;
            }
        }
        private string schemaMasterOwner;

        /// <summary>
        /// The DN of the NTDS Settings object of PDC Emulator role owner. (Not the DN of the Computer object).
        /// The PDC Emulator master processes password changes from client computers and replicates these updates 
        /// to all domain controllers throughout the domain.
        /// 
        /// To provide consistent password experience for users across sites (can be turned off with AvoidPdcOnWan 
        /// registry parameter) - The PDC emulator is used as a reference DC to double-check incorrect passwords and it 
        /// also receives new password changes. When the PDC is reachable, users can use a new password immediately 
        /// and consistently across the environment.
        /// 
        /// As a preferred point of administration for services (Group Policy and Distributed File System, DFS).
        /// 
        /// As a default time server for all other DCs in the domain - The time server configuration of a PDC requires 
        /// manual consideration and should be reviewed when you change the owner of the PDC role.
        /// </summary>
        /// <returns>Distinguished Name (DN)</returns>
        public string PDCEmulatorOwner {
            get {
                if (string.IsNullOrWhiteSpace(pdcEmulatorOwner)) {
                    var filter = string.Format("(objectCategory={0})", DomainProperties.ObjectCategoryShortName);
                    var domainList = ADDataAccess.GetADObjectProperties<DomainProperties>(
                        this.DistinguishedName, null, filter, PartitionScope.LocalPartition);
                    if ((domainList != null) && (domainList.Count == 1)) {
                        pdcEmulatorOwner = domainList.Single().FSMORoleOwner;
                    }
                }

                return pdcEmulatorOwner;
            }
        }
        private string pdcEmulatorOwner;

        /// <summary>
        /// The RID master allocates sequences of relative IDs (RIDs) to each of the various domain controllers 
        /// in its domain. At any time, there can be only one domain controller acting as the RID master in each domain 
        /// in the forest.
        /// 
        /// Whenever a domain controller creates a user, group, or computer object, it assigns the object a unique 
        /// security ID (SID). The SID consists of a domain SID, which is the same for all SIDs created in the domain, 
        /// and a RID, which is unique for each SID created in the domain.
        /// 
        /// To move an object between domains (using Movetree.exe), you must initiate the move on the domain controller 
        /// acting as the RID master of the domain that currently contains the object.
        /// </summary>
        /// <returns>Distinguished Name (DN)</returns>
        public string RIDManagerOwner {
            get {
                var filter = string.Format("(objectCategory={0})", RIDManager.ObjectCategoryShortName);
                string ridManagerDN = string.Format("CN=Rid Manager$,CN=System,{0}", DistinguishedName);
                var ridManagerList = ADDataAccess.GetADObjectProperties<RIDManager>(
                    ridManagerDN, null, filter, PartitionScope.LocalPartition);
                if ((ridManagerList != null) && (ridManagerList.Count == 1)) {
                    ridManagerOwner = ridManagerList.Single().FSMORoleOwner;
                }

                return ridManagerOwner;
            }
        }
        private string ridManagerOwner;

        /// <summary>
        ///  The infrastructure master is responsible for updating references from objects in its domain 
        ///  to objects in other domains. The infrastructure master compares its data with that of a global catalog. 
        ///  Global catalogs receive regular updates for objects in all domains through replication, 
        ///  so the global catalog data will always be up to date. If the infrastructure master finds data 
        ///  that is out of date, it requests the updated data from a global catalog. The infrastructure master 
        ///  then replicates that updated data to the other domain controllers in the domain.
        ///  
        /// Unless there is only one domain controller in the domain, the infrastructure master role should not 
        /// be assigned to the domain controller that is hosting the global catalog. 
        /// 
        /// If the infrastructure master and global catalog are on the same domain controller, the infrastructure master 
        /// will not function. The infrastructure master will never find data that is out of date, so it will never 
        /// replicate any changes to the other domain controllers in the domain.
        /// 
        /// In the case where all of the domain controllers in a domain are also hosting the global catalog, 
        /// all of the domain controllers will have the current data and it does not matter which domain controller 
        /// holds the infrastructure master role.
        /// 
        /// The infrastructure master is also responsible for updating the group-to-user references whenever the members 
        /// of groups are renamed or changed. When you rename or move a member of a group (and that member resides in a 
        /// different domain from the group), the group may temporarily appear not to contain that member. 
        /// The infrastructure master of the group's domain is responsible for updating the group so it knows the new name
        /// or location of the member. This prevents the loss of group memberships associated with a user account when 
        /// the user account is renamed or moved. The infrastructure master distributes the update via multimaster 
        /// replication.
        /// </summary>
        /// <returns>Distinguished Name (DN)</returns>
        public string InfrastractureManagerOwner {
            get {
                var filter = string.Format("(objectCategory={0})", InfrastructureManager.ObjectCategoryShortName);
                string infrastructureManagerDN = string.Format("CN=Infrastructure,{0}", DistinguishedName);
                var infrastructureManagerList = ADDataAccess.GetADObjectProperties<InfrastructureManager>(
                    infrastructureManagerDN, null, filter, PartitionScope.LocalPartition);
                if ((infrastructureManagerList != null) && (infrastructureManagerList.Count == 1)) {
                    infrastractureManagerOwner = infrastructureManagerList.Single().FSMORoleOwner;
                }

                return infrastractureManagerOwner;
            }
        }
        private string infrastractureManagerOwner;

        public string PDCEmulatorDnsName {
            get {
                if ((this.DomainDCs == null) || (this.DomainDCs.Count == 0)) return null;

                return this.DomainDCs
                    .Where(x => this.PDCEmulatorOwner.ToUpper().Contains(x.Substring(0, x.IndexOf(".")).ToUpper()))
                    .FirstOrDefault();
            }
        }

        public string RIDManagerDnsName {
            get {
                if ((this.DomainDCs == null) || (this.DomainDCs.Count == 0)) return null;

                return this.DomainDCs
                    .Where(x => this.RIDManagerOwner.ToUpper().Contains(x.Substring(0, x.IndexOf(".")).ToUpper()))
                    .FirstOrDefault();
            }
        }

        public string InfrastractureManagerDnsName {
            get {
                if ((this.DomainDCs == null) || (this.DomainDCs.Count == 0)) return null;

                return this.DomainDCs
                    .Where(x => this.InfrastractureManagerOwner.ToUpper().Contains(x.Substring(0, x.IndexOf(".")).ToUpper()))
                    .FirstOrDefault();
            }
        }
        #endregion
        #endregion

        #region Constructors
        public DomainConfiguration() {
            this.DistinguishedName = string.Empty;
            this.DNSName = string.Empty;
            this.ForestName = string.Empty;
            this.NetbiosName = string.Empty;
        }

        public DomainConfiguration(string distinguishedName, string dnsDomainName, string forestName, string netbiosDomainName, SecurityIdentifier sid, Guid domainGuid, bool isRootDomain) {
            this.DistinguishedName = distinguishedName;
            this.DNSName = dnsDomainName;
            this.ForestName = forestName;
            this.IsRootDomain = isRootDomain;
            this.NetbiosName = netbiosDomainName;
            this.SID = sid;
            this.DomainGuid = domainGuid;
        }
        #endregion

        #region Methods
        [DebuggerStepThroughAttribute]
        public override string ToString() {
            return this.DistinguishedName;
        }
        #endregion

    }
}
